/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Heavily inspired by the original js library copyright Mixpanel, Inc.
 * (http://mixpanel.com/)
 *
 * Copyright (c) 2012 Carl Sverre
 *
 * Released under the MIT license.
 */

import http from '@ohos.net.http';
import Log from './log.js';

const {asyncAll, ensureTimestamp, Base64} = require('./utils');
const {MixpanelGroups} = require('./groups');
const {MixpanelPeople} = require('./people');

const DEFAULT_CONFIG = {
  test: false,
  debug: false,
  verbose: false,
  host: 'api.mixpanel.com',
  protocol: 'https',
  path: '',
};

var serialize = function (obj, prefix) {
  var str = [],
    p;
  for (p in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, p)) {
      var k = prefix ? prefix + '[' + p + ']' : p,
        v = obj[p];
      str.push((v !== null && typeof v === 'object') ?
        serialize(v, k) : encodeURIComponent(k) + '=' + encodeURIComponent(v));
    }
  }
  return str.join('&');
};


var createClient = function (token, config) {
  if (!token) {
    throw new Error('The Mixpanel Client needs a Mixpanel token: `init(token)`');
  }

  // mixpanel constants
  const MAX_BATCH_SIZE = 50;
  const TRACK_AGE_LIMIT = 60 * 60 * 24 * 5;

  const metrics = {
    token,
    config: { ...DEFAULT_CONFIG },
  };

  /**
   * sends an async GET or POST request to mixpanel
   * for batch processes data must be send in the body of a POST
   * @param {object} options
   * @param {string} options.endpoint
   * @param {object} options.data         the data to send in the request
   * @param {string} [options.method]     e.g. `get` or `post`, defaults to `get`
   * @param {function} callback           called on request completion or error
   */
  metrics.sendRequest = function (options, callback) {
    callback = callback || function () {
    };

    let content = Base64.encode(JSON.stringify(options.data));
    const endpoint = options.endpoint;
    const method = (options.method || 'GET').toUpperCase();
    let queryParams = {
      'ip': 0,
      'verbose': metrics.config.verbose ? 1 : 0
    };
    const key = metrics.config.key;
    const secret = metrics.config.secret;
    let requestOptions = {
      host: metrics.config.host,
      port: metrics.config.port,
      headers: {},
      method: method
    };

    if (method === 'POST') {
      content = 'data=' + content;
      requestOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
    } else if (method === 'GET') {
      queryParams.data = content;
    }

    // add auth params
    if (secret) {
      const encoded = Base64.encode(secret + ':');
      requestOptions.headers['Authorization'] = 'Basic ' + encoded;
    } else if (key) {
      queryParams.apiKey = key;
    } else if (endpoint === '/import') {
      throw new Error('The Mixpanel Client needs a Mixpanel API Secret when importing ' +
              'old events: `init(token, { secret: ... })`');
    }

    if (metrics.config.test) {
      queryParams.test = 1;
    }

    requestOptions.path = metrics.config.path + endpoint + '?' + serialize(queryParams);
    var httpCarrier = http.createHttp();
    var parsedUrl = 'https://' + requestOptions.host + requestOptions.path;
    var httpOptions = {
      method: requestOptions.method,
      header: requestOptions.headers,
    };

    if (method === 'POST') {
      httpOptions['extraData'] = content;
    }

    httpCarrier.request(parsedUrl, httpOptions, (error, data) => {
      if (error == null) {
        var e;
        if (metrics.config.verbose) {
          try {
            var result = JSON.parse(data.result);
            if (result.status != 1) {
              e = new Error('Mixpanel Server Error: ' + result.error);
            }
          }
          catch (ex) {
            e = new Error('Could not parse response from Mixpanel');
          }
        }
        else {
          Log.showInfo('data.result: ' + data.result);
          e = (data.result !== '1') ? new Error('Mixpanel Server Error: ' + data) : undefined;
        }

        callback(e);
      } else {
        if (metrics.config.debug) {
          Log.showInfo('Got Error: ' + e.message);
        }
        callback(error);
      }
    });
  };

  /**
   * Send an event to Mixpanel, using the specified endpoint (e.g., track/import)
   * @param {string} endpoint - API endpoint name
   * @param {string} event - event name
   * @param {object} properties - event properties
   * @param {Function} [callback] - callback for request completion/error
   */
  metrics.sendEventRequest = function (endpoint, event, properties, callback) {
    properties.token = metrics.token;
    properties.mpLib = 'node';

    var data = {
      event: event,
      properties: properties
    };

    if (metrics.config.debug) {
      Log.showInfo('Sending the following event to Mixpanel:\n', data);
    }

    metrics.sendRequest({ method: 'GET', endpoint: endpoint, data: data }, callback);
  };

  /**
   * breaks array into equal-sized chunks, with the last chunk being the remainder
   * @param {Array} arr
   * @param {number} size
   * @returns {Array}
   */
  var chunk = function (arr, size) {
    var chunks = [],
      i = 0,
      total = arr.length;

    while (i < total) {
      chunks.push(arr.slice(i, i += size));
    }
    return chunks;
  };

  /**
   * sends events in batches
   * @param {object}   options
   * @param {[{}]}     options.eventList                 array of event objects
   * @param {string}   options.endpoint                   e.g. `/track` or `/import`
   * @param {number}   [options.maxConcurrentRequests]  limits concurrent async requests over the network
   * @param {number}   [options.maxBatchSize]           limits number of events sent to mixpanel per request
   * @param {Function} [callback]                         callback receives array of errors if any
   *
   */
  var sendBatchRequests = function (options, callback) {
    var eventList = options.eventList,
      endpoint = options.endpoint,
      maxBatchSize = options.maxBatchSize ? Math.min(MAX_BATCH_SIZE, options.maxBatchSize) : MAX_BATCH_SIZE,

      /**
       * to maintain original intention of maxBatchSize; if maxBatchSize is greater than 50,
       * we assume the user is trying to set maxConcurrentRequests
       */
      maxConcurrentRequests = options.maxConcurrentRequests ||
                (options.maxBatchSize > MAX_BATCH_SIZE && Math.ceil(options.maxBatchSize / MAX_BATCH_SIZE)),
      eventBatches = chunk(eventList, maxBatchSize),
      requestBatches = maxConcurrentRequests ? chunk(eventBatches, maxConcurrentRequests) : [eventBatches],
      totalEventBatches = eventBatches.length,
      totalRequestBatches = requestBatches.length;

    /**
     * sends a batch of events to mixpanel through http api
     * @param {Array} batch
     * @param {Function} cb
     */
    function sendEventBatch(batch, cb) {
      if (batch.length > 0) {
        batch = batch.map(function (event) {

          if (endpoint === '/import' || event.properties.time) {
            // usually there will be a time property, but not required for `/track` endpoint
            event.properties.time = ensureTimestamp(event.properties.time);
          }
          event.properties.token = event.properties.token || metrics.token;
          return event;
        });

        // must be a POST
        metrics.sendRequest({ method: 'POST', endpoint: endpoint, data: batch }, cb);
      }
    }

    /**
     * Asynchronously sends batches of requests
     * @param {number} index
     */
    function sendNextRequestBatch(index) {
      var requestBatch = requestBatches[index],
        cb = function (errors, results) {
          index += 1;
          if (index === totalRequestBatches) {
            callback && callback(errors, results);
          } else {
            sendNextRequestBatch(index);
          }
        };

      asyncAll(requestBatch, sendEventBatch, cb);
    }

    // init recursive function
    sendNextRequestBatch(0);

    if (metrics.config.debug) {
      Log.showInfo(
        'Sending ' + eventList.length + ' events to Mixpanel in ' +
        totalEventBatches + ' batches of events and ' +
        totalRequestBatches + ' batches of requests'
      );
    }
  };

  /**
   * track(event, properties, callback)
   * ---
   * this function sends an event to mixpanel.
   *
   * event:string                    the event name
   * properties:object               additional event properties to send
   * callback:function(err:Error)    callback is called when the request is
   *                                 finished or an error occurs
   */
  metrics.track = function (event, properties, callback) {
    if (!properties || typeof properties === 'function') {
      callback = properties;
      properties = {};
    }

    // time is optional for `track` but must be less than 5 days old if set
    if (properties.time) {
      properties.time = ensureTimestamp(properties.time);
      if (properties.time < Date.now() / 1000 - TRACK_AGE_LIMIT) {
        throw new Error('`track` not allowed for event more than 5 days old; use `mixpanel.import()`');
      }
    }

    metrics.sendEventRequest('/track', event, properties, callback);
  };

  /**
   * send a batch of events to mixpanel `track` endpoint: this should only be used if events are less than 5 days old
   * @param {Array}    eventList                         array of event objects to track
   * @param {object}   [options]
   * @param {number}   [options.maxConcurrentRequests]  number of concurrent http requests that can be made to mixpanel
   * @param {number}   [options.maxBatchSize]           number of events that can be sent to mixpanel per request
   * @param {Function} [callback]                         callback receives array of errors if any
   */
  metrics.trackBatch = function (eventList, options, callback) {
    options = options || {};
    if (typeof options === 'function') {
      callback = options;
      options = {};
    }
    var batchOptions = {
      eventList: eventList,
      endpoint: '/track',
      maxConcurrentRequests: options.maxConcurrentRequests,
      maxBatchSize: options.maxBatchSize
    };

    sendBatchRequests(batchOptions, callback);
  };

  /**
   *
   * import(event, time, properties, callback)
   * ---
   * This function sends an event to mixpanel using the import
   * endpoint.  The time argument should be either a Date or Number,
   * and should signify the time the event occurred.
   *
   * It is highly recommended that you specify the distinctId
   * property for each event you import, otherwise the events will be
   * tied to the IP address of the sending machine.
   * For more information look at:
   * https://mixpanel.com/docs/api-documentation/importing-events-older-than-31-days
   * event:string                    the event name
   * time:date|number                the time of the event
   * properties:object               additional event properties to send
   * callback:function(err:Error)    callback is called when the request is
   *                                 finished or an error occurs
   */
  metrics.import = function (event, time, properties, callback) {
    if (!properties || typeof properties === 'function') {
      callback = properties;
      properties = {};
    }

    properties.time = ensureTimestamp(time);

    metrics.sendEventRequest('/import', event, properties, callback);
  };

  /**
   * importBatch(eventList, options, callback)
   * ---
   * This function sends a list of events to mixpanel using the import
   * endpoint. The format of the event array should be:
   *
   * [
   *     {
   *         'event': 'event name',
   *         'properties': {
   *             'time': new Date(), // Number or Date; required for each event
   *             'key': 'val',
   *             ...
   *         }
   *     },
   *     {
   *         'event': 'event name',
   *         'properties': {
   *             'time': new Date()  // Number or Date; required for each event
   *         }
   *     },
   *     ...
   * ]
   *
   * See import() for further information about the import endpoint.
   *
   * Options:
   *     maxBatchSize: the maximum number of events to be transmitted over
   *                     the network simultaneously. useful for capping bandwidth
   *                     usage.
   *     maxConcurrentRequests: the maximum number of concurrent http requests that
   *                     can be made to mixpanel; also useful for capping bandwidth.
   *
   * N.B.: the Mixpanel API only accepts 50 events per request, so regardless
   * of maxBatchSize, larger lists of events will be chunked further into
   * groups of 50.
   *
   * eventList:array                    list of event names and properties
   * options:object                      optional batch configuration
   * callback:function(errorList:array) callback is called when the request is
   *                                     finished or an error occurs
   */
  metrics.importBatch = function (eventList, options, callback) {
    var batchOptions;

    if (typeof (options) === 'function' || !options) {
      callback = options;
      options = {};
    }
    batchOptions = {
      eventList: eventList,
      endpoint: '/import',
      maxConcurrentRequests: options.maxConcurrentRequests,
      maxBatchSize: options.maxBatchSize
    };
    sendBatchRequests(batchOptions, callback);
  };

  /**
   * alias(distinctId, alias)
   * ---
   * This function creates an alias for distinctId
   *
   * For more information look at:
   * https://mixpanel.com/docs/integration-libraries/using-mixpanel-alias
   *
   * distinctId:string              the current identifier
   * alias:string                    the future alias
   */
  metrics.alias = function (distinctId, alias, callback) {
    var properties = {
      distinctId: distinctId,
      alias: alias
    };

    metrics.track('$create_alias', properties, callback);
  };

  metrics.groups = new MixpanelGroups(metrics);
  metrics.people = new MixpanelPeople(metrics);

  /**
   *
   * setConfig(config)
   * ---
   * Modifies the mixpanel config
   *
   * config:object       an object with properties to override in the
   *                     mixpanel client config
   */
  metrics.setConfig = function (config) {
    Object.assign(metrics.config, config);
    if (config.host) {
      // Split host into host and port
      const [host, port] = config.host.split(':');
      metrics.config.host = host;
      if (port) {
        metrics.config.port = Number(port);
      }
    }
  };

  if (config) {
    metrics.setConfig(config);
  }

  return metrics;
};

var mixpanel = { init: createClient, 'MixpanelGroups': MixpanelGroups, 'MixpanelPeople': MixpanelPeople };

export default mixpanel;
